﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
using WoltersKluwer.SPChainGang.Core.Configuration;
using WoltersKluwer.SPChainGang.Core.Constants;
using WoltersKluwer.SPChainGang.Core.ExtensionMethods;
using WoltersKluwer.SPChainGang.Core.ObjectScanners;
using WoltersKluwer.SPChainGang.Core.Util;
using System.IO;

namespace WoltersKluwer.SPChainGang.Core.TimerJobs
{
    public class ChainGangJob : SPJobDefinition
    {
        const int lockCheckPercent = 1;
        const int scanStartPercent = 2;
        const int scanEndPercent = 99;

        public ChainGangJob()
        {
        }

        public ChainGangJob(string title, SPWebApplication webApplication, SPServer server)
            : base(
                String.Format(
                    GlobalConstants.ChainGangJobNameTemplate, DateTime.Now.ToString(GlobalConstants.TimeStampPattern)),
                webApplication, server, SPJobLockType.Job)
        {
            Title = title;
        }

        public JobMode Operation
        {
            get
            {
                return GetFromPropertyBag<JobMode>("JobMode");
            }
            set
            {
                PutInPropertyBag("JobMode", value);
            }
        }
        
        public Guid SiteCollectionId
        {
            get
            {
                return GetFromPropertyBag<Guid>("SiteCollectionId");
            }
            set
            {
                PutInPropertyBag("SiteCollectionId", value);
            }
        }

        public string EmailsToNotify
        {
            get
            {
                return GetFromPropertyBag<string>("EmailsToNotify"); //.Split(';').ToList();
            }
            set
            {
                PutInPropertyBag("EmailsToNotify", value); //String.Join(";", value.ToArray()));
            }
        }

        public Guid ConfigurationListId
        {
            get
            {
                return GetFromPropertyBag<Guid>("ConfigurationListId");
            }
            set
            {
                PutInPropertyBag("ConfigurationListId", value);
            }
        }

        private T GetFromPropertyBag<T>(string key)
        {
            if (Properties.ContainsKey(key))
                return (T)Properties[key];

            return default(T);
        }

        private void PutInPropertyBag(string key, object val)
        {
            if (Properties.ContainsKey(key))
                Properties[key] = val;
            else
                Properties.Add(key, val);
        }

        private SPUserToken GetSystemToken()
        {
            using (var targetSiteCollection = new SPSite(SiteCollectionId))
                return targetSiteCollection.SystemAccount.UserToken;
        }

        public override void Execute(Guid targetInstanceId)
        {
            try
            {
                //impersonating just in case this job is launched from the web interface
                using (var targetSiteCollection = new SPSite(SiteCollectionId, GetSystemToken()))
                {
                    WaitForOthers(targetSiteCollection);
                    UpdateProgress(scanStartPercent);
                    Logger.Log(Id,
                               "job ({2}) started on server {3} in mode {0} scanning the site collection under URL '{1}'",
                               Operation, targetSiteCollection.Url, Title, SPServer.Local.Name);
                    var scanner = new CompositeScanner(Title, targetSiteCollection, ConfigurationListId, Operation, Id);
                    var webs = targetSiteCollection.AllWebs;
                    int totalWebs = webs.Count;
                    int processedWebs = 0;
                    foreach (SPWeb web in webs)
                    {
                        try
                        {
                            scanner.Scan(web);
                        }
                        catch(OutOfMemoryException e)
                        {
                            LogCrash(e, web.Title, web.ID, web.Url);
                            break;
                        }
                        catch (Exception e)
                        {
                            LogCrash(e, web.Title, web.ID, web.Url);
                        }
                        finally
                        {
                            web.Dispose();
                            processedWebs++;
                            UpdateProgress(scanStartPercent + processedWebs * (scanEndPercent - scanStartPercent) / totalWebs);
                        }
                    }

                    UpdateProgress(scanEndPercent);
                    Logger.Log(Id,
                               "Completed job ({2}) on server {3}, was running in mode {0} scanning the site collection under URL '{1}'",
                               Operation, targetSiteCollection.Url, Title, SPServer.Local.Name);
                    var reportUrl = UploadReport(scanner, targetSiteCollection);
                    SendEmailNotification(targetSiteCollection, reportUrl);
                }
            }
            finally
            {
                UpdateProgress(100); //percentage used in syncronous lock
            }
        }

        private void LogCrash(Exception e, params object[] args)
        {
            Logger.LogError(Id, String.Format("Crashed while scanning '{0}' web (ID:{1}, URL:{2})", args), e);
        }

        private void WaitForOthers(SPSite siteCollection)
        {
            do
            {
                if (RunningSameJobsCount(siteCollection) == 0)
                {
                    UpdateProgress(lockCheckPercent);
                    if (RunningSameJobsCount(siteCollection) == 1)
                        return;
                }
                UpdateProgress(0); //TODO: possible live lock
                Thread.Sleep(10000); //check every 10 seconds
            } while (true);
        }

        private int RunningSameJobsCount(SPSite siteCollection)
        {
            return
                siteCollection.WebApplication.RunningJobs.Cast<SPRunningJob>().Where(
                    runningJob => runningJob.PercentageDone > 0 && runningJob.PercentageDone < 100).Select(
                        runningJob => runningJob.JobDefinition as ChainGangJob).Where(
                            job => job != null && job.SiteCollectionId == SiteCollectionId).Count();
        }

        private void SendEmailNotification(SPSite targetSiteCollection, string reportUrl)
        {
            try
            {
                var configuration = new ConfigurationSettings();
                if (!EmailsToNotify.IsNullOrEmpty())
                {
                    var messageBody = String.Format(
                        configuration.MailBodyTemplate, Operation.ToFriendlyString(), targetSiteCollection.Url, reportUrl);
                    if (SPUtility.IsEmailServerSet(targetSiteCollection.RootWeb))
                    {
                        SPUtility.SendEmail(targetSiteCollection.RootWeb, true, false, EmailsToNotify, configuration.MailSubject,
                            messageBody);
                    }
                    else
                    {
                        Logger.Log(Id, String.Format("Email server is not configured for site: {0} ", targetSiteCollection.RootWeb.Url));
                    }
                }
            }
            catch (Exception e)
            {
                Logger.LogError(Id, "Unable to send email", e);
            }
        }

        private string UploadReport(ScannerBase scaner, SPSite targetSiteCollection)
        {
            string linkToReport = String.Empty;
            try
            {
                var configuration = new ConfigurationSettings();
                SPList reportsList = targetSiteCollection.RootWeb.Lists[GlobalConstants.ReportsListName];
                var reportItem = reportsList.Items.Add();
                var folderName = Path.GetFileNameWithoutExtension(scaner.ReportTempFilename);
                var reportDestName = String.Format("report{0}", Path.GetExtension(scaner.ReportTempFilename));
                const string logDestName = "log.txt";
                string logFileName = string.Concat(Path.GetTempPath(), Logger.LOG_FILE_NAME);

                try //first trying to upload in a document library
                {
                    SPList reportsLibrary = targetSiteCollection.RootWeb.Lists[GlobalConstants.ReportsLibraryName];
                    var reportFolder = reportsLibrary.RootFolder.SubFolders.Add(folderName);
                    var uploadedReport = SPDocumentLibraryHelper.UploadFileToDocumetnLibrary(reportFolder, scaner.ReportTempFilename, reportDestName);
                    linkToReport = SPUrlUtility.CombineUrl(targetSiteCollection.Url, uploadedReport.Url);
                    SPDocumentLibraryHelper.UploadFileToDocumetnLibrary(reportFolder, logFileName, logDestName);
                }
                catch (Exception e) //FTP upload as a last resort option
                {
                    Logger.LogError(Id, "Unable to upload report in the documents library", e);
                    var ftpPath = string.Format("/SPChainGang/{0}/{1}", folderName, reportDestName);
                    linkToReport = FTPHelper.UploadToFtp(ftpPath, scaner.ReportTempFilename, configuration.FTPHost,
                                                    configuration.FTPUserName, configuration.FTPPassword, false);
                    File.Delete(scaner.ReportTempFilename);
                    FTPHelper.UploadToFtp(string.Format("/SPChainGang/{0}/{1}", folderName, logDestName), logFileName, configuration.FTPHost, configuration.FTPUserName, configuration.FTPPassword);
                    File.Delete(logFileName);
                }
                if (!linkToReport.IsNullOrEmpty())
                {
                    reportItem["URL"] = string.Format("{0}, {1}", linkToReport,
                                                      Path.GetFileName(scaner.ReportTempFilename));
                    reportItem.Update();
                }
            }
            catch (Exception e)
            {
                Logger.LogError(Id, "Unable to upload report on FTP", e);
            }
            return linkToReport;
        }

        protected override bool HasAdditionalUpdateAccess()
        {
            return true;
        }
    }
}
